package org.apache.struts2.result.jaxb;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.dispatcher.StrutsResultSupport;
import org.apache.struts2.interceptor.jaxb.JAXBInterceptor;

import com.opensymphony.xwork2.ActionInvocation;

/**
 * <p>This object provides very simple marshaling from an object defined
 *  in the struts stack. The marshaling is done through JAXB so the 
 *  object must contain the proper JAXB setup before it can be
 *  marshaled.</p>
 * 
 * @author Matt Filion (www.mattfilion.com)
 *
 */
public class JAXBResultType extends StrutsResultSupport{

    protected static final Log              log               = LogFactory.getLog(JAXBResultType.class);
    private static Map<String, JAXBContext> jaxbContexts      = new HashMap<String, JAXBContext>();

    /**
     * Default Serialization Id 1L
     */
    private static final long               serialVersionUID  = 1L;

    private static final String             CONTENT_TYPE      = "text/xml";

    private boolean                         gzipResult        = false;
    private boolean                         isOutputFormatted = true;

    private String                          packageContext    = null;
    private String                          objectToMarshall  = null;

    @Override
    protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
        
        OutputStream output        = null;
        String       packageConext = null;
        Object       object        = null;
        
        try{

            output = prepareForResponse(invocation);

            packageConext = getPackageContext(invocation);
            object        = getObject(invocation);
            
            if(object!=null){
                JAXBContext jaxbContext = getJAXBContext( packageConext );
    
                // Get the XML representation of the objectToBeMarshalled
                Marshaller marshaller = jaxbContext.createMarshaller();
                if(isOutputFormatted){
                    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
                }
                marshaller.marshal( object, output );

            } else {
                String erroMessage = "<error>Something is not configured correctly with your result type. No object could be found to marshall. Check the application logs for more information.</error>";
                output.write(erroMessage.getBytes());
                
            }

            // Flush the output
            output.flush();

        } catch(JAXBException jaxbE){
            /*
             * An attempt to provide a somewhat meaningful error if the 
             *  marshalling is unsuccessful.
             */
            StringBuilder exceptionMessage = new StringBuilder().append("<exception>")
                    .append("<type>").append(jaxbE.getClass().getName()).append("</type>" )
                    .append("<message>").append(jaxbE.getMessage()).append("</message>" )
                    .append("<gzipResult>").append(gzipResult).append("</gzipResult>" )
                    .append("<isOutputFormatted>").append(isOutputFormatted).append("</isOutputFormatted>" )
                    .append("<objectToMarshall>").append(objectToMarshall).append("</objectToMarshall>" )
                    .append("<packageConext>").append(packageConext).append("</packageConext>");
            if(object!=null){
                exceptionMessage.append("<object>").append(object.getClass().getName()).append("</object>");
            }
            
            exceptionMessage.append("</exception>");
            
            output.write(exceptionMessage.toString().getBytes());
            output.flush();
            
            log.error("A problem occoured while marshalling.",jaxbE);
            
        } finally {
            if (output != null) output.close();
        }
    }
    
    /**
     * Prepares the response and gets the OutputStream to be used to 
     *  stream the object.
     *
     * @param invocation
     * @return
     * @throws IOException
     */
    private OutputStream prepareForResponse(ActionInvocation invocation) throws IOException{
        
        boolean isGzipOkay = isGzipOkay(invocation);
        
        HttpServletResponse httpResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE);

        // Set the content type
        httpResponse.setContentType(CONTENT_TYPE);
        
        // Set the content-disposition
        String contentDisposition = "inline;filename=\"document="+getClass().getSimpleName() + ".xml\"";
        
        httpResponse.setHeader("Content-disposition", contentDisposition);
        httpResponse.setHeader("Content-Encoding", "gzip");

        if(gzipResult && isGzipOkay){
            log.debug("Using a GZIPOutputStream wrapped around the http response output stream");
            return new GZIPOutputStream(httpResponse.getOutputStream());
        } else {
            log.debug("Using a standard response output stream");
            return httpResponse.getOutputStream();
        }
    }
    
    /**
     * Finds the package context.
     * @param invocation
     * @return
     */
    private String getPackageContext(ActionInvocation invocation){
        //First Look on the object.
        if(packageContext==null){
            packageContext = conditionalParse(this.packageContext, invocation);
        }

        //Next check the stack for an Intercepter provided package context
        if(packageContext==null){
            //First look in the stack.
            packageContext = (String) invocation.getStack().findValue("_packageContext");
            log.debug("Package context was provided by the JAXB interceptor.");
        }

        if(packageContext==null || packageContext.equals("")){
            log.error("No package context could be found.");
            throw new IllegalArgumentException("No package context could be found.");
        }

        return packageContext;
    }
    
    /**
     * Finds the object to be marshaled.
     * @param invocation
     * @return
     */
    @SuppressWarnings("unchecked")
    private Object getObject(ActionInvocation invocation){
        
        if(objectToMarshall==null){
            log.info("Your result type is not properly configured. You must have a <param name=objectToMarshall>{object}</param> defined on the result definition.");
            return null;
        }

        Object object = invocation.getStack().findValue(conditionalParse(objectToMarshall, invocation));

        if(object!=null || object instanceof String){
            object = conditionalParse(objectToMarshall, invocation);
        }
        
        if(object==null || object instanceof String){
            object = invocation.getStack().findValue(objectToMarshall);
        }
        
        if(object==null || object instanceof String){
            log.info("Had to result in reflection to get the object to marshall.");
            /*
             * Okay I have no idea why those other steps didnt find the object
             *  its time to use reflection directly.
             */
            Object        actionObject = invocation.getAction();
            StringBuilder builder      = new StringBuilder();

            builder.append("get").append(objectToMarshall.substring(0,1).toUpperCase()).append(objectToMarshall.substring(1));
            try {
                Method method = actionObject.getClass().getMethod(builder.toString(), (Class) null);
                object = method.invoke(actionObject, (Object) null);
            } catch(Throwable t){
                //This is a pretty lame way about going things, so just swallow anything that might occur.
            }
        }
        if(object==null){
            log.info("No object was found that is going to be marshalled to XML.");
        }
        
        return object;
    }
    
    /**
     * Checks the request to make sure that gzip encoding is okay.
     * 
     * @param requests
     * @return
     */
    @SuppressWarnings("unchecked")
    private boolean isGzipOkay(ActionInvocation invocation){
        
        HttpServletRequest  httpRequest  = (HttpServletRequest) invocation.getInvocationContext().get(HTTP_REQUEST);
        Enumeration<String> accepted     = httpRequest.getHeaders("Accept-Encoding");

        while(accepted.hasMoreElements()){
            String headerValue=(String)accepted.nextElement();
            log.debug("Accept-Encoding: "+ headerValue);
            if(headerValue.indexOf("gzip")!=-1){
                log.debug("Gzip Encoding is okay");
                return true;
            }
        }
        return false;
    }
    
    /**
     * Returns the JAXBContext
     * @param contextPath
     * @return
     */
    private JAXBContext getJAXBContext(String contextPath){
        if(contextPath==null){
            return null;
        }
        
        /*
         * Try the Interceptor first, its most likely already cached there.
         */
        JAXBContext jaxbContext =  JAXBInterceptor.findJAXBContext(contextPath);

        if(jaxbContext==null){
            jaxbContext = jaxbContexts.get(contextPath);
        }
        
        if(jaxbContext==null){
            try {
                jaxbContext = JAXBContext.newInstance(contextPath);
                jaxbContexts.put(contextPath, jaxbContext);
            } catch (JAXBException e) {
                log.error("There was a problem while creating the JAXBContext");
            }
        }
        
        return jaxbContext;
    }

    /**
     * @return the gzipResult
     */
    public boolean isGzipResult() {
        return gzipResult;
    }

    /**
     * @param gzipResult the gzipResult to set
     */
    public void setGzipResult(boolean gzipResult) {
        this.gzipResult = gzipResult;
    }

    /**
     * @return the isOutputFormatted
     */
    public boolean isOutputFormatted() {
        return isOutputFormatted;
    }

    /**
     * @param isOutputFormatted the isOutputFormatted to set
     */
    public void setOutputFormatted(boolean isOutputFormatted) {
        this.isOutputFormatted = isOutputFormatted;
    }

    /**
     * @return the packageContext
     */
    public String getPackageContext() {
        return packageContext;
    }

    /**
     * @param packageContext the packageContext to set
     */
    public void setPackageContext(String packageContext) {
        this.packageContext = packageContext;
    }

    /**
     * @return the objectToMarshall
     */
    public String getObjectToMarshall() {
        return objectToMarshall;
    }

    /**
     * @param objectToMarshall the objectToMarshall to set
     */
    public void setObjectToMarshall(String objectToMarshall) {
        this.objectToMarshall = objectToMarshall;
    }
    
}
